问题
在枚举类型出现之前,一般都常常使用
int
常量或者String
常量表示列举相关事物。如:public static final int APPLE_FUJI = 0; public static final int APPLE_PIPPIN = 1; public static final int APPLE_GRANNY_SMITH = 2; public static final int ORANGE_NAVEL = 0; public static final int ORANGE_TEMPLE = 1; public static final int ORANGE_BLOOD = 2;1234567
针对
int
常量以下不足:- 在类型安全方面,如果你想使用的是
ORANGE_NAVEL
,但是传递是APPLE_FUJI
,编译器并不能检测出错误; - 因为
int
常量是编译时常量,被编译到使用它们的客户端中。若与枚举常量关联的int
发生了变化,客户端需重新编译,否则它们的行为就不确定; - 没有便利方法将
int
常量翻译成可打印的字符串。这里的意思应该是比如你想调用的是ORANGE_NAVEL
,debug的时候显示的是0,但你不能确定是APPLE_FUJI
还是ORANGE_NAVEL
; - 如果你想使用
String
常量,虽然可以打印,但是字符串的比较是对性能有较大的影响的。另外,容易将字符串硬编码到代码之中;
那么,针对这样的情况应该怎样解决?
- 在类型安全方面,如果你想使用的是
解决
针对这样的情况,可以采用
enum
来解决。enum使用方法
enum
默认构建以上面的
APPLE
、ORANGE
为例,用enum
重写:public enum Apple { APPLE_FUJI, APPLE_PIPPIN, APPLE_GRANNY_SMITH; } public enum Orange { ORANGE_NAVEL, ORANGE_TEMPLE, ORANGE_BLOOD; }
在调用的时候,直接使用
enum
类型,在编译的时候可以直接指定类型,否则编译不通过;并且debug的时候,显示的是enum
中的常量(APPLE_FUJI
这样的),可以一眼看出是否用错;最后由于枚举导出的常量域(APPLE_FUJI
等)与客户端之间是通过枚举来引用的,再增加或者重排序枚举类型中的常量后,并不需要重新编译客户端代码enum
枚举数据域和方法在
enum
类中同样可以有自己的数据域和方法。如以太阳系为例,每个行星都拥有质量和半径,可以依据这两个属性计算行星表面物体的重量。代码如下:
public enum Planet { MERCURY(3.302e+23, 2.439e6), VENUS (4.869e+24, 6.052e6), EARTH (5.975e+24, 6.378e6), MARS (6.419e+23, 3.393e6), JUPITER(1.899e+27, 7.149e7), SATURN (5.685e+26, 6.027e7), URANUS (8.683e+25, 2.556e7), NEPTUNE(1.024e+26, 2.477e7); private final double mass; // In kilograms private final double radius; // In meters private final double surfaceGravity; // In m / s^2 // Universal gravitational constant in m^3 / kg s^2 private static final double G = 6.67300E-11; // Constructor Planet(double mass, double radius) { this.mass = mass; this.radius = radius; surfaceGravity = G * mass / (radius * radius); } public double mass() { return mass; } public double radius() { return radius; } public double surfaceGravity() { return surfaceGravity; } public double surfaceWeight(double mass) { return mass * surfaceGravity; // F = ma } } public class PlanetDemo { public static void main(String[] args) { double earthWeight = Double.parseDouble(args[0]); double mass = earthWeight / Planet.EARTH.surfaceGravity(); for (Planet p : Planet.values()) { System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass)); } //args[0]=30输出结果 //Weight on MERCURY is 11.337201 //Weight on VENUS is 27.151530 //Weight on EARTH is 30.000000 //Weight on MARS is 11.388120 //Weight on JUPITER is 75.890383 //Weight on SATURN is 31.965423 //Weight on URANUS is 27.145664 //Weight on NEPTUNE is 34.087906 } }
enum
枚举常量来分发不同的方法有时候会使用枚举中的值来作为逻辑条件来分发至不同的方法,如采用枚举来写加、减、乘、除的运算。代码如下:
public enum Operation { PLUS, MINUS, TIMES, DIVIDE; double apply(double x, double y) { switch(this) { case PLUS: return x + y; case MINUS: return x - y; case TIMES: return x * y; case DIVIDE: return x / y; } throw new AssertionError("Unknown op: " + this); } }
大家一开始都会这样写的。实际开发中,有很多开发者也这样写。但是有个不足:如果需要新增加运算,譬如模运算,不仅仅需要添加枚举类型常量,还需要修改
apply
方法。万一忘记修改了,那就是运行时错误。将代码修改如下:public enum Operation { PLUS { @Override double apply(double x, double y) { return x + y; } }, MINUS { @Override double apply(double x, double y) { return x - y; } }, TIMES { @Override double apply(double x, double y) { return x * y; } }, DIVIDE { @Override double apply(double x, double y) { return x / y; } }; abstract double apply(double x, double y); }
每次新增加运算种类,都需要重写
apply
方法,这样就不会有遗漏修改。你可以写的更详细些:
public enum Operation { PLUS("+") { @Override double apply(double x, double y) { return x + y; } }, MINUS("-") { @Override double apply(double x, double y) { return x - y; } }, TIMES("*") { @Override double apply(double x, double y) { return x * y; } }, DIVIDE("/") { @Override double apply(double x, double y) { return x / y; } }; private String symbol; Operation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } abstract double apply(double x, double y); } public class OperationDemo { public static void main(String[] args) { double x = Double.parseDouble(args[0]); double y = Double.parseDouble(args[1]); for (Operation op : Operation.values()) { System.out.println(String.format("%f %s %f = %f%n", x, op, y, op.apply(x, y))); } //输入2 4 //2.000000 + 4.000000 = 6.000000 //2.000000 - 4.000000 = -2.000000 //2.000000 * 4.000000 = 8.000000 //2.000000 / 4.000000 = 0.500000 } }
一般,
enum
中重写了toString
方法之后,enum
中自生成的valueOf(String)
方法不能根据枚举常量的字符串(toString
生成)来获取枚举常量。我们通常需要在enum
中新增个静态常量来获取。如:public enum Operation { PLUS("+") { @Override double apply(double x, double y) { return x + y; } }, MINUS("-") { @Override double apply(double x, double y) { return x - y; } }, TIMES("*") { @Override double apply(double x, double y) { return x * y; } }, DIVIDE("/") { @Override double apply(double x, double y) { return x / y; } }; private String symbol; public static final Map<String, Operation> OPERS_MAP = Maps.newHashMap(); static { for (Operation op : Operation.values()) { OPERS_MAP.put(op.toString(), op); } } Operation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } abstract double apply(double x, double y); }
可以通过调用
Operation.OPERS_MAP.get(op.toString())
来获取对应的枚举常量。在有些特定的情况下,此写法有个缺点,即如果每个枚举常量都有公共的部分处理该怎么办,如果每个枚举常量关联的方法里都有公共的部分,那不仅不美观,还违反了DRY原则。这就是下面的枚举策略模式。
枚举策略模式
直接上例子来分析:
enum PayrollDay { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY; private static final int HOURS_PER_SHIFT = 8; double pay(double hoursWorked, double payRate) { double basePay = hoursWorked * payRate; double overtimePay; // Calculate overtime pay switch(this) { case SATURDAY: case SUNDAY: overtimePay = hoursWorked * payRate / 2; break; default: // Weekdays overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2; } return basePay + overtimePay; } }
以上代码是计算工人工资。平时工作8小时,超过8小时,以加班工资方式另外计算;如果是双休日,都按照加班方式处理工资。上面代码的写法和上一小节给出的差不多,通过
switch
来分拆计算。还是一样的问题,如果此时新增加一种工资的计算方式,枚举常量需要改,pay
方法也需要改。按上一小节的介绍继续修改:enum PayrollDay { MONDAY { @Override double pay(double hoursWorked, double payRate) { double basePay = hoursWorked * payRate; double overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2; return basePay + overtimePay; } }, TUESDAY { @Override double pay(double hoursWorked, double payRate) { double basePay = hoursWorked * payRate; double overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2; return basePay + overtimePay; } }, WEDNESDAY { @Override double pay(double hoursWorked, double payRate) { double basePay = hoursWorked * payRate; double overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2; return basePay + overtimePay; } }, THURSDAY { @Override double pay(double hoursWorked, double payRate) { double basePay = hoursWorked * payRate; double overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2; return basePay + overtimePay; } }, FRIDAY { @Override double pay(double hoursWorked, double payRate) { double basePay = hoursWorked * payRate; double overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2; return basePay + overtimePay; } }, SATURDAY { @Override double pay(double hoursWorked, double payRate) { double basePay = hoursWorked * payRate; double overtimePay = overtimePay = hoursWorked * payRate / 2; return basePay + overtimePay; } }, SUNDAY { @Override double pay(double hoursWorked, double payRate) { double basePay = hoursWorked * payRate; double overtimePay = overtimePay = hoursWorked * payRate / 2; return basePay + overtimePay; } }, ; private static final int HOURS_PER_SHIFT = 8; abstract double pay(double hoursWorked, double payRate); }
看了上面的代码,我觉得大家都不会这样写吧。其实细想一下,最主要的不同就是计算加班时间的工资方式不同,也就是分工作日和双休日的。继续修改:
public enum PayRoll { MONDY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY), SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND); private final PayType payType; PayRoll(PayType payType) { this.payType = payType; } double pay(double hoursWorked, double payRate) { return payType.pay(hoursWorked, payRate); } private enum PayType { WEEKDAY { @Override double overtimePay(double hoursWorked, double payRate) { double overtime = hoursWorked - HOURS_PER_SHIFT; return overtime <= 0 ? 0 : overtime * payRate / 2; } }, WEEKEND { @Override double overtimePay(double hoursWorked, double payRate) { return hoursWorked * payRate / 2; } }; private static final int HOURS_PER_SHIFT = 8; abstract double overtimePay(double hoursWorked, double payRate); double pay(double hoursWorked, double payRate) { double basePay = hoursWorked * payRate; return basePay + overtimePay(hoursWorked, payRate); } } }
虽然看起来代码不够简洁,但是修改起来确实比较安全,不怕有遗漏。
补充一点
从上面可以看出,不推荐在
enum
中使用switch...case...
来判断不同的行为。那什么时候可以使用呢?主要是适用于给外部的枚举类型增加特定于常量的行为。如,假设Operation
枚举不受开发者自己控制,但是希望它有一个实例方法来返回每个运算的反运算,则可以:public static Operation inverse(Operation op) { switch(op) { case PLUS: return Operation.MINUS; case MINUS: return Operation.PLUS; case TIMES: return Operation.DIVIDE; case DIVIDE: return Operation.TIMES; default: throw new AssertionError("Unknown op: " + op); } }
结论
与int常量相比,枚举的可读性更强,并且更加安全,功能更加强大,在实际开发中应该使用enum代替int枚举模式。